package com.kiki.song.recommend;

import com.kiki.song.domain.main.Song;
import com.kiki.song.domain.recommend.*;
import com.kiki.song.mapper.SongMapper;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

/**
 * 实时推荐引擎
 * @author kiki
 * @date 2023/03/09
 */
public class OnLineRecommendationEngine {
    // 内存数据存储（生产环境应使用Redis）
    private final ConcurrentMap<String, UserProfile> userProfiles = new ConcurrentHashMap<>();//其他用户画像
    private final ConcurrentMap<String, SongProfile> songMetadata = new ConcurrentHashMap<>();//歌曲元数据

    // 注入SongMapper
    private SongMapper songMapper;

    // 使用构造函数注入bean
    public OnLineRecommendationEngine(SongMapper songMapper) {
        this.songMapper = songMapper;
    }

    // 初始化加载歌曲元数据
    public void initializeSongs(List<Song>songList) {
        for (Song song : songList) {
            songMetadata.put(String.valueOf(song.getSongId()), new SongProfile(song.getSongId(), song.getSinger(), song.getType()));
        }
    }

    /**
     * 启动处理流程
     * eventQueue1为"本"用户的行为队列
     * userProfiles1为其他用户画像
     *
     */
    public List<Song> startProcessing(List<UserProfile> userProfiles1,int userId) {
        //将userProfiles1赋值给userProfiles
        for (UserProfile userProfile : userProfiles1){
            userProfiles.put(userProfile.getUserId(), userProfile);
        }
        // 1. 从队列获取事件（阻塞式），take() 方法会从队列中获取并移除队列头部的元素，第一次获取"本"用户第一个行为事件
        System.out.println("实时触发推荐前");//没问题
        List<Song> recommendations = generateRecommendations(String.valueOf(userId));//生成推荐结果
        for (Song song : recommendations){
            //打印song对象，除了它的歌词属性lyric
            System.out.println("歌曲名称为："+song.getSongName()+"歌手为："+song.getSinger());
        }
//            System.out.println("截取基于用户和基于物品总合后的十个推荐结果 ：" + recommendations);
        System.out.println("实时触发推荐后");
        System.out.println("单线程中处理事件结束");
        //*最终return recommendations给用户*
        return recommendations;
    }


    /**
     * 生成混合推荐结果
     */
    private List<Song> generateRecommendations(String userId) {

//            System.out.println("指定id的UserProfile为:"+userProfiles.get(userId));

        //获取userId用户的个人档案
        UserProfile user = userProfiles.get(userId);

        // 并行计算两种推荐结果
        Set<String> userBased = userBasedRecommendation(user);//基于用户推荐
        Set<String> itemBased = itemBasedRecommendation(user);//基于物品推荐

        // 新增代码：构建并打印用户相似度矩阵
        Map<String, Map<String, Double>> similarityMatrix = buildUserSimilarityMatrix();
        System.out.println("\n===== 用户相似度矩阵 =====");
        printSimilarityMatrix(similarityMatrix);

        System.out.println("user："+userBased);
        System.out.println("item："+itemBased);

        // 混合策略：优先物品推荐，补充用户相似推荐
        Set<String> finalRecs = new LinkedHashSet<>();
        finalRecs.addAll(itemBased);
        finalRecs.addAll(userBased);
        // 过滤用户已播放歌曲
        finalRecs.removeAll(user.getRecentPlays());

        System.out.println("user/item：" + finalRecs.size());
        // 将推荐结果转换为Song对象列表
        List<Song> recommendationsList = new ArrayList<>();
        for (String songId : finalRecs) {
            Song song = songMapper.selectSongBySongId(Long.parseLong(songId));
            recommendationsList.add(song);
        }
        System.out.println("混合推荐结果(理论上是10个，但是如果user或者item两者中有重复则数量少于10个)：" + recommendationsList.size());
        return recommendationsList;
    }

    /**
     * 基于用户的协同过滤推荐
     * 策略：找到相似用户（根据收藏和搜索行为），推荐他们听的歌曲
     */
    private Set<String> userBasedRecommendation(UserProfile user) {
        Set<String> recommendations = new HashSet<>();//推荐结果
        System.out.println("=================基于用户===================");
//            for (UserProfile other : userProfiles.values()){
//                System.out.println("所有的user的值为："+other);
//            }
        // 遍历所有用户找相似（生产环境应用聚类或索引优化）
        userProfiles.values().stream()
                .filter(other -> !other.getUserId().equals(user.getUserId())) // 排除自己，other为userProfiles的一个对象
                .sorted(Comparator.comparingDouble(other -> {
                    System.out.println("other的值为："+other);
                    return calculateUserSimilarity(user, (UserProfile) other);//有收藏，也有关键词
                }).reversed()) // 默认是按照小到大排序，.reversed()则是从大到小排序
                .limit(2) // 取Top2相似用户档案
                .forEach(similarUser -> {  //similarUser==>other
                    recommendations.addAll(similarUser.getRecentPlays());
                    System.out.println("相似用户有2，其一是："+similarUser);//1=》68 2=》62 3=》69 4=》31
                });
        System.out.println("基于用户的协同过滤的推荐结果(截取6个)："+recommendations.stream().limit(6).collect(Collectors.toSet()));

        //截取recommendations其中的6个结果，并返回
        return recommendations.stream().limit(6).collect(Collectors.toSet());
    } //100->0.31 101->0 102->0 103->0.31


    /**
     * 计算用户相似度（余弦相似度）
     * 余弦相似度 = 点积/（u1模长*u2模长的根号）
     * 点积 = u1和u2的交集数量
     * 模长 = u1和u2的各自属性值开根号（如果是关键词）
     * @param u1
     * @param u2
     * @return
     */
    private Double calculateUserSimilarity(UserProfile u1, UserProfile u2) {
        synchronized (this) {
        // 计算收藏列表的余弦相似度
        Set<String> commonFavorites = new HashSet<>(u1.getFavorites());
        commonFavorites.retainAll(u2.getFavorites()); // 获取两个用户收藏列表的交集，保留 commonFavorites 中与用户 u2 收藏歌曲 ID 相同的元素
        double favoriteDotProduct = commonFavorites.size(); // 交集大小作为点积
        double u1FavNorm = Math.sqrt(u1.getFavorites().size()); // 用户u1收藏列表的模
        double u2FavNorm = Math.sqrt(u2.getFavorites().size()); // 用户u2收藏列表的模
        double favSimilarity = (u1FavNorm == 0 || u2FavNorm == 0) ? 0 : // 如果任一用户没有收藏，相似度为0
                favoriteDotProduct / (u1FavNorm * u2FavNorm); // 计算余弦相似度
        // 计算搜索关键词的余弦相似度（带权重）
        double searchDotProduct = 0.0;
        double u1SearchNorm = 0.0, u2SearchNorm = 0.0;

        Map<String, Integer> u1Search = u1.getSearchKeywords(); // 用户u1的搜索关键词及其出现次数
        Map<String, Integer> u2Search = u2.getSearchKeywords(); // 用户u2的搜索关键词及其出现次数
        for (Map.Entry<String, Integer> entry : u1Search.entrySet()) {
            System.out.println("关键词相似度计算进行中。。");
            String keyword = entry.getKey(); // 关键词
            int count1 = entry.getValue(); // keyword关键词在用户u1中该关键词的出现次数
            int count2 = u2Search.getOrDefault(keyword, 0); // keyword关键词在用户u2中该关键词的出现次数
            System.out.println("关键词：'"+keyword+"'，用户1：搜索了"+count1+"次，用户2：搜索了"+count2+"次");
            searchDotProduct += count1 * count2; // 计算点积, 即两个关键词出现的次数之积
            u1SearchNorm += count1 * count1; // 计算用户u1的搜索关键词向量的模的平方
            System.out.println("点积："+searchDotProduct);
            System.out.println("用户1的模："+u1SearchNorm);
        }
        // 计算用户u2的搜索关键词向量的模
        for (Map.Entry<String, Integer> entry : u2Search.entrySet()) {
            //前面的count1和count2是用来计算两者的点积（即交集数量），所以此处不用再计算了，此处是用来计算模长的
            int count = entry.getValue(); // 用户u2中该关键词的出现次数
            u2SearchNorm += count * count; // 计算用户u2的搜索关键词向量的模的平方

        }
        u1SearchNorm = Math.sqrt(u1SearchNorm); // 计算用户u1的搜索关键词向量的模
        u2SearchNorm = Math.sqrt(u2SearchNorm); // 计算用户u2的搜索关键词向量的模
        double searchSimilarity = (u1SearchNorm == 0 || u2SearchNorm == 0) ? 0 : // 如果任一用户没有搜索记录，相似度为0
                searchDotProduct / (u1SearchNorm * u2SearchNorm); // 计算余弦相似度
        // 综合加权计算最终相似度
        return favSimilarity * 0.7 + searchSimilarity * 0.3;
        }
    }

    /**
     * 基于物品的特征推荐
     * 策略：根据用户最近播放歌曲的特征推荐相似歌曲
     */
    private Set<String> itemBasedRecommendation(UserProfile user) { //注意这里的用户有很多听过的歌曲，所以会推荐所有听过的歌曲的歌手和类型
        System.out.println("=================基于物品===================");
        Set<String> recommendations = new HashSet<>();//使用set集合，去重
        // 分析最近播放特征("收藏"属性和"搜索历史"属性作为“基于物品的特征推荐”中的参考，这里使用播放历史属性作为参考)
        user.getRecentPlays().forEach(songId -> {
            System.out.println("用户收听历史歌曲id："+songId);
            SongProfile song = songMetadata.get(songId);
            //如果歌曲不存在，则跳过该歌曲
            if (song == null) {
                return;
            }

            /**
             * 基于物品的特征推荐
             * 策略：根据用户最近播放歌曲的特征推荐相似歌曲
             * 通过改user用户的的最近播放的歌曲，找到音乐列表中和它同一艺术家的歌曲以及同一类型（比如童谣）的歌曲
             * songMetadata.values()是songMetadata中所有歌曲的集合，使用parallelStream()方法来并行处理集合中的元素。
             */
            // 策略1：推荐相同歌手
            songMetadata.values().parallelStream() //获取 songMetadata 中所有值，并使用并行流进行操作
                    .filter(s -> s.getArtist().equals(song.getArtist())&& !s.getSongId().toString().equals(songId))//获取相同演唱者的歌曲，同时不能和本歌曲id一样
                    .limit(3)//取Top3
                    .map(s -> s.getSongId())// 将每个SongProfile对象映射为它的songId属性
                    .forEach(songId1 -> {
                        System.out.println("推荐相同艺术家的歌曲：" + songId1);
                        recommendations.add(String.valueOf(songId1));
                    });
            // 策略2：推荐相同类型
            songMetadata.values().parallelStream()
                    .filter(s -> s.getType().equals(song.getType())&& !s.getSongId().toString().equals(songId))//获取相同演唱者的歌曲，同时不能和本歌曲id一样
                    .limit(3)//取Top3
                    .map(s -> s.getSongId())
                    .forEach(songId2 ->
                    {
                        System.out.println("推荐相同流派的歌曲：" + songId2);
                        recommendations.add(String.valueOf(songId2));
                    });
        });
        System.out.println("基于物品的特征推荐结果为:"+recommendations.stream().limit(4).collect(Collectors.toSet()));
        //截取recommendations集合的前4个元素，并返回一个新集合
        return recommendations.stream().limit(4).collect(Collectors.toSet());
    }


    // 新增方法：构建用户相似度矩阵
    public Map<String, Map<String, Double>> buildUserSimilarityMatrix() {
        Map<String, Map<String, Double>> similarityMatrix = new ConcurrentHashMap<>();

        // 获取所有用户ID
        List<String> userIds = new ArrayList<>(userProfiles.keySet());

        // 遍历所有用户对
        userIds.parallelStream().forEach(userId1 -> {
            UserProfile u1 = userProfiles.get(userId1);
            Map<String, Double> row = new ConcurrentHashMap<>();

            userIds.forEach(userId2 -> {
                if (userId1.equals(userId2)) {
                    // 用户与自身相似度为1（最大值）
                    row.put(userId2, 1.0);
                } else {
                    UserProfile u2 = userProfiles.get(userId2);
                    // 计算相似度（复用现有方法）
                    double similarity = calculateUserSimilarity(u1, u2);
                    row.put(userId2, similarity);
                }
            });

            similarityMatrix.put(userId1, row);
        });

        return similarityMatrix;
    }

    // 新增方法：打印相似度矩阵
    public void printSimilarityMatrix(Map<String, Map<String, Double>> similarityMatrix) {
        // 获取排序后的用户ID列表（保证输出顺序一致）
        List<String> userIds = similarityMatrix.keySet().stream()
                .sorted(Comparator.comparing(Long::parseLong))
                .toList();

        // 打印表头
        System.out.print("UserID\t");
        userIds.forEach(id -> System.out.print(id + "\t\t\t"));
        System.out.println();

        // 打印每行数据
        for (String userId1 : userIds) {
            System.out.print(userId1 + "\t\t");
            Map<String, Double> row = similarityMatrix.get(userId1);
            for (String userId2 : userIds) {
                double similarity = row.getOrDefault(userId2, 0.0);
                System.out.printf("%.2f\t\t", similarity);
            }
            System.out.println();
        }
    }


    // 新增方法：构建用户-物品评分矩阵
    public Map<Long, Map<Long, Integer>> buildRatingMatrix() {
        Map<Long, Map<Long, Integer>> ratingMatrix = new HashMap<>();

        // 遍历所有用户
        for (UserProfile user : userProfiles.values()) {
            Long userId = Long.parseLong(user.getUserId());
            Map<Long, Integer> userRatings = new HashMap<>();

            // 获取用户行为数据
            List<String> recentPlays = new ArrayList<>(user.getRecentPlays());
            List<String> favorites = user.getFavorites();
            Map<String, Integer> searchKeywords = user.getSearchKeywords();

            // 遍历所有歌曲，计算评分
            for (SongProfile song : songMetadata.values()) {
                Long songId = song.getSongId();
                int score = 0;

                // 规则1: 最近播放记录（每条播放记录+1分）
                score += Collections.frequency(recentPlays, songId.toString());//遍历的对比最近播放的歌曲id，有的话，就加1分

                // 规则2: 收藏记录（每条收藏+3分）
                for(String favorite : favorites){
                    if (favorite.equals(songId.toString())) {
                        score += 3;
                    }
                }
//                if (favorites.contains(songId.toString())) {
//                    score += 3;
//                }

                // 规则3: 搜索关键词匹配（每个匹配的关键词+2分）
                for (String keyword : searchKeywords.keySet()) {
                    // 如果歌曲的歌手或类型包含关键词，则给该歌曲加2分
                    if (song.getArtist().contains(keyword) || song.getType().contains(keyword)) {
                        score += 2 * searchKeywords.get(keyword); // 关键词出现次数加权
                    }
                }

                userRatings.put(songId, score);
            }

            ratingMatrix.put(userId, userRatings);
        }

        return ratingMatrix;
    }
}
